Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mesh creation from primitive shapes #11007

Closed
wants to merge 38 commits into from

Conversation

Jondolf
Copy link
Contributor

@Jondolf Jondolf commented Dec 17, 2023

Objective

⚠️ Warning: This PR is not meant to be merged by itself. The purpose is to provide a high-level overview of the proposed approach and changes, and to split it into more granular PRs once the approach has been validated.

The goal is to implement Mesh creation for the recently added primitive shapes (#10572), superseding the old set of shapes in bevy_render::mesh::shape. The approach includes a Meshable trait as suggested by the original RFC.

As we rework the mesh shapes, we also have an opportunity to reach some secundary but important goals:

  • More shapes: Almost all primitives have mesh creation implemented. This is a lot more than the old set of shapes.
  • Document everything: All structs, methods and properties in the shape module are properly documented. The module now has a #![warn(missing_docs)] clause.
  • Increase consistency: Some names like "segments" and "resolution" are currently used interchangeably, and APIs aren't very consistent. The default dimensions of shapes are also quite arbitrary. I'm attempting to unify this a bit.
  • Different XYZ orientations for 2D shapes: You can now create rectangles, circles, etc. that face the positive or negative X, Y, or Z direction, similarly to Godot's facing direction. This is also useful for the goal below.
  • Refactor logic to be more reusable: For example, the bases of cones, cylinders, and so on can use circle meshes instead of always reimplementing the logic.

Closes #10569 (although this might be done across several PRs)

Solution

There is now a Meshable trait:

/// A trait for shapes that can be turned into a [`Mesh`].
pub trait Meshable {
    /// The output of [`Self::mesh`]. This can either be a [`Mesh`]
    /// or a builder used for creating a [`Mesh`].
    type Output;

    /// Creates a [`Mesh`] for a shape.
    fn mesh(&self) -> Self::Output;
}

It's implemented for all of the primitive shapes that support meshing, i.e. currently all of these:

  • Capsule
  • Circle
  • Cone
  • ConicalFrustum
  • Cuboid
  • Cylinder
  • Ellipse
  • Plane3d
  • Rectangle
  • RegularPolygon
  • Sphere
  • Torus
  • Triangle2d

Each shape has its own FooMesh version that is returned by the mesh method. These separate structs are required for configuration like specifying the resolution or UV profile, and it uses a builder-like API for ease of use.

For example, the CapsuleMesh, which looks like the current Capsule mesh struct, except it uses the Capsule primitive:

/// A builder used for creating a [`Mesh`] with a [`Capsule`] shape.
#[derive(Clone, Copy, Debug)]
pub struct CapsuleMesh {
    pub capsule: Capsule,
    pub rings: usize,
    pub longitudes: usize,
    pub latitudes: usize,
    pub uv_profile: CapsuleUvProfile,
}

The Mesh postfix is used for clarity and to avoid naming conflicts, but it could be CapsuleBuilder or CapsuleMeshBuilder too.

It has constructors and builder methods to make configuration easier:

impl CapsuleMesh {
    pub fn new(radius: f32, height: f32, longitudes: usize, latitudes: usize) -> Self { ... }
    pub const fn rings(mut self, rings: usize) -> Self { ... }
    pub const fn longitudes(mut self, longitudes: usize) -> Self { ... }
    pub const fn latitudes(mut self, latitudes: usize) -> Self { ... }
    pub const fn uv_profile(mut self, uv_profile: CapsuleUvProfile) -> Self { ... }

    /// Builds a [`Mesh`] based on the configuration in `self`.
    pub fn build(&self) -> Mesh { ... }
}

Using it looks like this:

// These are equivalent
let mesh = CapsuleMesh::new(0.5, 1.2, 32, 16);
let mesh = Capsule::new(0.5, 1.2).mesh().longitudes(32).latitudes(16);

meshes.add(mesh.build());

Note that build can even be omitted when adding meshes like meshes.add(...) if #10878 gets merged. From<Capsule> and From<CapsuleMesh> are also implemented for Mesh, so into would work as well.

2D meshes now also support different orientations with the Facing enum to face any of the three cartesian axes:

/// The cartesian axis that a [`Mesh`] should be facing upon creation.
/// This is either positive or negative `X`, `Y`, or `Z`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Facing {
    X = 1,
    Y = 2,
    #[default]
    Z = 3,
    NegX = -1,
    NegY = -2,
    NegZ = -3,
}

// ...

// These are equivalent
let mesh = Rectangle::new(1.0, 0.5).mesh().facing(Facing::X);
let mesh = Rectangle::new(1.0, 0.5).mesh().facing_x();

This is similar to Godot's facing direction for quads and also doubles as a way to flip meshes. Another reason why this is useful is that we can use it to refactor internal mesh logic to be more reusable. For example, we can use CircleMesh for constructing the top and bottom of a CylinderMesh, but just flip the direction the circle is facing.

Below are a few screenshots showing all of the supported shapes with different orientations for 2D shapes.

With UV visualization:

uv

With textures:

texture

With wireframes:

wireframe

The implementation details of each mesh can be outlined in more detail in their own PRs if I split this PR into granular parts.

Missing mesh implementations

Lines and Direction2d/Direction3d currently don't support meshing because they would require wireframe materials to be rendered (unless we model lines as e.g. capsules or cylinders with thickness) and it's generally better done with gizmos.

Polygon doesn't support meshing because it requires triangulation, which is complex to do well and would most likely require external dependencies. I've tried using earcutr, geo, and lyon_tessellation for this, but none of them seem to support both self-intersections and boolean operations at the same time correctly.

Meshes for these can be implemented later.

API alternatives

The API I opted for is builder-like and has separate structs for the mesh versions of shapes. Some other options I considered include:

  • No unified Meshable trait or separate structs, just implement mesh creation for each shape separately like Sphere::new(0.5).uv(longitudes, latitudes)
    • Pro: Very simple and clean at best
    • Pro: No excess data stored :)
    • Con: No data stored :( People might want to pass mesh configurations around
    • Con: All configuration must be passed at once; for something like a capsule, this is four properties!
    • Con: Documentation for configuration properties can not be done cleanly
    • Con: No default values for mesh shapes
    • Con: API is not unified
  • Hard-code global defaults, so you can just do Mesh::from(Sphere::new(0.5))
    • Pro: Simple
    • Con: Almost all cons of the previous option
    • Con: No configuration obviously, which is very bad. The resolution, UVs, etc. need to be configurable.

Compare this to the builder API:

  • Sphere::new(0.5).mesh().uv(longitudes, latitudes) or SphereMesh::new(radius, SphereKind::Uv { longitudes, latitudes }).build()
    • Pro: Very explicit
    • Pro: Concise. build can even be omitted when adding meshes like meshes.add(...) if Use impl Into<A> for Assets::add #10878 gets merged
    • Pro: Configuration is stored and can be reused
    • Pro: Good documentation is trivial because the properties are actual properties
    • Pro: Easy to extend in the future with more properties
    • Con: Storing configuration isn't always necessary
    • Con: A lot more structs in Bevy

I believe the builder-like API is the ideal middle-ground between simplicity, ease of use, and configurability, and I can't think of many major downsides. Feel free to leave suggestions though!

Open questions

  • There are now two ways to do the same thing, for example primitives::Sphere::new(0.5).mesh().uv(longitudes, latitudes) and shapes::SphereMesh::new(radius, SphereKind::Uv { longitudes, latitudes }).build(). Do we want to keep the Meshable API and have these coexist, or should we perhaps remove it? And which approach should we recommend and use in examples?
  • Not all primitives have configuration, e.g. cuboids. Should these still have mesh versions like CuboidMesh for the sake of consistency? Personally, I think they should.
  • Other concerns?

Next steps

If this approach is approved, I can submit granular PRs to add all of the functionality in this PR in chunks that are easier to review independently. The PRs might be roughly like this:

  • Add Meshable trait and mesh creation for 2D primitives
  • Implement Mesh creation for Sphere, Cylinder, Capsule, and Torus (the new versions of the "old" 3D shapes)
  • Implement Mesh creation for Cone and ConicalFrustum
  • Implement Mesh creation for Plane3d
  • Remove old mesh shapes in favor of the new primitives

Once all of this is done, we should implement meshing for the remaining primitives, add similar support for gizmos (#10571) and finish other remaining items in #10572.

@Jondolf Jondolf added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Dec 17, 2023
Copy link
Contributor

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Could you add some context on what users should update when this change get released in a new version of Bevy?
It will be used to help writing the migration guide for the version. Putting it after a ## Migration Guide will help it get automatically picked up by our tooling.

@Jondolf Jondolf force-pushed the mesh-from-primitives branch from 738bfea to a0c42ce Compare January 9, 2024 19:34
github-merge-queue bot pushed a commit that referenced this pull request Jan 29, 2024
# Objective

The first part of #10569, split up from #11007.

The goal is to implement meshing support for Bevy's new geometric
primitives, starting with 2D primitives. 3D meshing will be added in a
follow-up, and we can consider removing the old mesh shapes completely.

## Solution

Add a `Meshable` trait that primitives need to implement to support
meshing, as suggested by the
[RFC](https://github.com/bevyengine/rfcs/blob/main/rfcs/12-primitive-shapes.md#meshing).

```rust
/// A trait for shapes that can be turned into a [`Mesh`].
pub trait Meshable {
    /// The output of [`Self::mesh`]. This can either be a [`Mesh`]
    /// or a builder used for creating a [`Mesh`].
    type Output;

    /// Creates a [`Mesh`] for a shape.
    fn mesh(&self) -> Self::Output;
}
```

This PR implements it for the following primitives:

- `Circle`
- `Ellipse`
- `Rectangle`
- `RegularPolygon`
- `Triangle2d`

The `mesh` method typically returns a builder-like struct such as
`CircleMeshBuilder`. This is needed to support shape-specific
configuration for things like mesh resolution or UV configuration:

```rust
meshes.add(Circle { radius: 0.5 }.mesh().resolution(64));
```

Note that if no configuration is needed, you can even skip calling
`mesh` because `From<MyPrimitive>` is implemented for `Mesh`:

```rust
meshes.add(Circle { radius: 0.5 });
```

I also updated the `2d_shapes` example to use primitives, and tweaked
the colors to have better contrast against the dark background.

Before:

![Old 2D
shapes](https://github.com/bevyengine/bevy/assets/57632562/f1d8c2d5-55be-495f-8ed4-5890154b81ca)

After:

![New 2D
shapes](https://github.com/bevyengine/bevy/assets/57632562/f166c013-34b8-4752-800a-5517b284d978)

Here you can see the UVs and different facing directions: (taken from
#11007, so excuse the 3D primitives at the bottom left)

![UVs and facing
directions](https://github.com/bevyengine/bevy/assets/57632562/eaf0be4e-187d-4b6d-8fb8-c996ba295a8a)

---

## Changelog

- Added `bevy_render::mesh::primitives` module
- Added `Meshable` trait and implemented it for:
  - `Circle`
  - `Ellipse`
  - `Rectangle`
  - `RegularPolygon`
  - `Triangle2d`
- Implemented `Default` and `Copy` for several 2D primitives
- Updated `2d_shapes` example to use primitives
- Tweaked colors in `2d_shapes` example to have better contrast against
the (new-ish) dark background

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
tjamaan pushed a commit to tjamaan/bevy that referenced this pull request Feb 6, 2024
…ine#11431)

# Objective

The first part of bevyengine#10569, split up from bevyengine#11007.

The goal is to implement meshing support for Bevy's new geometric
primitives, starting with 2D primitives. 3D meshing will be added in a
follow-up, and we can consider removing the old mesh shapes completely.

## Solution

Add a `Meshable` trait that primitives need to implement to support
meshing, as suggested by the
[RFC](https://github.com/bevyengine/rfcs/blob/main/rfcs/12-primitive-shapes.md#meshing).

```rust
/// A trait for shapes that can be turned into a [`Mesh`].
pub trait Meshable {
    /// The output of [`Self::mesh`]. This can either be a [`Mesh`]
    /// or a builder used for creating a [`Mesh`].
    type Output;

    /// Creates a [`Mesh`] for a shape.
    fn mesh(&self) -> Self::Output;
}
```

This PR implements it for the following primitives:

- `Circle`
- `Ellipse`
- `Rectangle`
- `RegularPolygon`
- `Triangle2d`

The `mesh` method typically returns a builder-like struct such as
`CircleMeshBuilder`. This is needed to support shape-specific
configuration for things like mesh resolution or UV configuration:

```rust
meshes.add(Circle { radius: 0.5 }.mesh().resolution(64));
```

Note that if no configuration is needed, you can even skip calling
`mesh` because `From<MyPrimitive>` is implemented for `Mesh`:

```rust
meshes.add(Circle { radius: 0.5 });
```

I also updated the `2d_shapes` example to use primitives, and tweaked
the colors to have better contrast against the dark background.

Before:

![Old 2D
shapes](https://github.com/bevyengine/bevy/assets/57632562/f1d8c2d5-55be-495f-8ed4-5890154b81ca)

After:

![New 2D
shapes](https://github.com/bevyengine/bevy/assets/57632562/f166c013-34b8-4752-800a-5517b284d978)

Here you can see the UVs and different facing directions: (taken from
bevyengine#11007, so excuse the 3D primitives at the bottom left)

![UVs and facing
directions](https://github.com/bevyengine/bevy/assets/57632562/eaf0be4e-187d-4b6d-8fb8-c996ba295a8a)

---

## Changelog

- Added `bevy_render::mesh::primitives` module
- Added `Meshable` trait and implemented it for:
  - `Circle`
  - `Ellipse`
  - `Rectangle`
  - `RegularPolygon`
  - `Triangle2d`
- Implemented `Default` and `Copy` for several 2D primitives
- Updated `2d_shapes` example to use primitives
- Tweaked colors in `2d_shapes` example to have better contrast against
the (new-ish) dark background

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
github-merge-queue bot pushed a commit that referenced this pull request Feb 6, 2024
# Objective

Split up from #11007, fixing most of the remaining work for #10569.

Implement `Meshable` for `Cuboid`, `Sphere`, `Cylinder`, `Capsule`,
`Torus`, and `Plane3d`. This covers all shapes that Bevy has mesh
structs for in `bevy_render::mesh::shapes`.

`Cone` and `ConicalFrustum` are new shapes, so I can add them in a
follow-up, or I could just add them here directly if that's preferrable.

## Solution

Implement `Meshable` for `Cuboid`, `Sphere`, `Cylinder`, `Capsule`,
`Torus`, and `Plane3d`.

The logic is mostly just a copy of the the existing `bevy_render`
shapes, but `Plane3d` has a configurable surface normal that affects the
orientation. Some property names have also been changed to be more
consistent.

The default values differ from the old shapes to make them a bit more
logical:

- Spheres now have a radius of 0.5 instead of 1.0. The default capsule
is equivalent to the default cylinder with the sphere's halves glued on.
- The inner and outer radius of the torus are now 0.5 and 1.0 instead of
0.5 and 1.5 (i.e. the new minor and major radii are 0.25 and 0.75). It's
double the width of the default cuboid, half of its height, and the
default sphere matches the size of the hole.
- `Cuboid` is 1x1x1 by default unlike the dreaded `Box` which is 2x1x1.

Before, with "old" shapes:


![old](https://github.com/bevyengine/bevy/assets/57632562/733f3dda-258c-4491-8152-9829e056a1a3)

Now, with primitive meshing:


![new](https://github.com/bevyengine/bevy/assets/57632562/5a1af14f-bb98-401d-82cf-de8072fea4ec)

I only changed the `3d_shapes` example to use primitives for now. I can
change them all in this PR or a follow-up though, whichever way is
preferrable.

### Sphere API

Spheres have had separate `Icosphere` and `UVSphere` structs, but with
primitives we only have one `Sphere`.

We need to handle this with builders:

```rust
// Existing structs
let ico = Mesh::try_from(Icophere::default()).unwrap();
let uv = Mesh::from(UVSphere::default());

// Primitives
let ico = Sphere::default().mesh().ico(5).unwrap();
let uv = Sphere::default().mesh().uv(32, 18);
```

We could add methods on `Sphere` directly to skip calling `.mesh()`.

I also added a `SphereKind` enum that can be used with the `kind`
method:

```rust
let ico = Sphere::default()
    .mesh()
    .kind(SphereKind::Ico { subdivisions: 8 })
    .build();
```

The default mesh for a `Sphere` is an icosphere with 5 subdivisions
(like the default `Icosphere`).

---

## Changelog

- Implement `Meshable` and `Default` for `Cuboid`, `Sphere`, `Cylinder`,
`Capsule`, `Torus`, and `Plane3d`
- Use primitives in `3d_shapes` example

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
@Jondolf Jondolf mentioned this pull request Feb 9, 2024
@Jondolf
Copy link
Contributor Author

Jondolf commented Jun 16, 2024

Closing in favor of #11688 and the dozen other primitive meshing PRs that have already been merged ages ago :P

@Jondolf Jondolf closed this Jun 16, 2024
@Jondolf Jondolf deleted the mesh-from-primitives branch June 16, 2024 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Meshes from primitives
1 participant